﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using MathNet.Numerics;
using MathNet.Numerics.LinearAlgebra.Double;
using System.Diagnostics;

namespace T2GenerativeClassifiers
{
#region POINT
    public class Point
    {
        DenseVector _x;
        int _y;

        public DenseVector X_Vector { get { return _x; } }
        public int Y { get { return _y; } }

        public Point(DenseVector x_value, int alpha_value, string label)
        {
            _x = x_value;
            _y = Program.labels[label];
        }
        public Point(string s)
        {
            var elems = s.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            var d_components = new double[elems.Length - 1];
            for (int i = 0; i < d_components.Length; i++)
            {
                d_components[i] = double.Parse(elems[i]);
            }
            _x = new DenseVector(d_components);
            _y = Program.labels[elems[elems.Length - 1]];// == "Iris-setosa" ? 1.0 : -1.0;
        }

        public override string ToString()
        {
            return "X : " + _x + ", Y : " + _y;
        }
    }
#endregion

#region GDM
    public class MultivariateNormalDistributionClassifier
    {
        private double[] _fis;
        private DenseVector[] _mis;
        private DenseMatrix _sigma;
        private double _costant_part;
        private double _vector_dim;
        private int _num_types;
        private DenseMatrix _sigma_inverse;

        public double[] Fis { get { return _fis; } }
        public DenseVector[] Mis { get { return _mis; } }
        public DenseMatrix Sigma { get { return _sigma; } }

        public MultivariateNormalDistributionClassifier(int num_types, int elem_size)
        {
            _vector_dim = elem_size;
            _num_types = num_types;
            _fis = new double[_num_types];
            _mis = new DenseVector[_num_types];
            _sigma = new DenseMatrix(elem_size);
            for (int i = 0; i < _num_types; i++)
            {
                _fis[i] = 0;
                _mis[i] = new DenseVector(elem_size);
            }
        }

        public void CreateFromPoints(Point[] points)
        {
            double m = (double)points.Length;
            int[] groups = new int[_num_types];
            foreach (var p in points)
            {
                groups[p.Y]++;
                _mis[p.Y] += p.X_Vector;
            }
            for (int i = 0; i < _num_types; i++)
            {
                _fis[i] = groups[i] / m;
                _mis[i] /= groups[i];
            }

            foreach (var p in points)
            {
                var u = (p.X_Vector - _mis[p.Y]);
                _sigma += DenseVector.OuterProduct(u, u);
            }
            _sigma = _sigma.Divide(m) as DenseMatrix;
            _costant_part = 1.0 / (Math.Pow((2 * Math.PI), _vector_dim / 2.0) * Math.Pow(_sigma.Determinant(), 1.0 / 2));
            _sigma_inverse = _sigma.Inverse() as DenseMatrix;
        }

        public int ClassifyThis(Point p)
        {
            var probs = new double[_num_types];
            for (int i = 0; i < _num_types; i++)
            {
                var diff = p.X_Vector - _mis[i];
                var mul = (diff * _sigma_inverse * diff);
                probs[i] = _fis[i] * _costant_part * Math.Exp(-(1.0 / 2.0) * mul);
            }
            int max = 0;
            double max_v = probs[0];
            for (int i = 1; i < _num_types; i++)
            {
                if (probs[i] > max_v)
                {
                    max_v = probs[i];
                    max = i;
                }
            }
            if (p.Y != max) ;
            return max;
        }

        public override string ToString()
        {
            return "Fis : " + _fis + " Mis : " + _mis + " Sigma : " + _sigma;
        }
    }
#endregion

#region NAIVE BAYES
    public class NaiveBayesClassifier
    {
        private double[] _fis;
        private DenseVector[] _mis;
        private DenseVector[] _sigmaTwos;
        private double _vector_dim;
        private int _num_types;

        public double[] Fis { get { return _fis; } }
        public DenseVector[] Mis { get { return _mis; } }
        public DenseVector[] SigmaTwos { get { return _sigmaTwos; } }

        public NaiveBayesClassifier(int num_types, int elem_size)
        {
            _vector_dim = elem_size;
            _num_types = num_types;
            _fis = new double[_num_types];
            _mis = new DenseVector[_num_types];
            _sigmaTwos = new DenseVector[num_types];
            for (int i = 0; i < _num_types; i++)
            {
                _fis[i] = 0;
                _mis[i] = new DenseVector(elem_size);
                _sigmaTwos[i] = new DenseVector(elem_size);
            }
        }

        public void CreateFromPoints(Point[] points)
        {
            double m = (double)points.Length;
            int[] groups = new int[_num_types];
            foreach (var p in points)
            {
                groups[p.Y]++;
                _mis[p.Y] += p.X_Vector;
            }
            for (int i = 0; i < _num_types; i++)
            {
                _fis[i] = groups[i] / m;
                _mis[i] /= groups[i];
            }

            foreach (var p in points)
            {
                var u = (p.X_Vector - _mis[p.Y]);
                var prod = u.PointwiseMultiply(u) as DenseVector;
                _sigmaTwos[p.Y] += prod;
            }
            for (int i = 0; i < _num_types; i++)
            {
                _sigmaTwos[i] /= groups[i];
            }
        }

        public int ClassifyThis(Point p)
        {
            var probs = new double[_num_types];
            for (int k = 0; k < _num_types; k++)
            {
                probs[k] = _fis[k];
                for (int i = 0; i < p.X_Vector.Count; i++)
                {
                    var sigmai = _sigmaTwos[k][i];
                    var xi = p.X_Vector[i];
                    var miki= _mis[k][i];
                    var fix = 1.0 / Math.Sqrt(2 * Math.PI * sigmai);
                    var exp = Math.Exp(-(1.0 / (2.0 * sigmai) * (Math.Pow(xi - miki, 2.0))));
                    probs[k] *= fix * exp;
                }
            }

            int max = 0;
            double max_v = probs[0];
            
            for (int i = 1; i < _num_types; i++)
            {
                if (probs[i] > max_v)
                {
                    max_v = probs[i];
                    max = i;
                }
            }
            if (p.Y != max) ; 
            return max;
        }

        public override string ToString()
        {
            return "Fis : " + _fis + " Mis : " + _mis + " Sigmas : " + _sigmaTwos;
        }
    }
#endregion

#region MAIN
    class Program
    {
        public static Dictionary<string, int> labels = new Dictionary<string,int>();
        public static Stopwatch chrono = new Stopwatch();
        private static Random seed = new Random();
        static void Main(string[] args)
        {
            System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture("en");
            System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture("en");

            var text = File.ReadAllLines("iris.data");

            labels.Add("Iris-setosa", 0);
            labels.Add("Iris-versicolor", 1);
            labels.Add("Iris-virginica", 2);
            Console.WriteLine("verbose?? (yes/no)");

            StreamWriter out_f = null;
            if (Console.ReadLine() == "no")
            {
                out_f = File.CreateText("res" + ".csv");
                Console.SetOut(out_f);
            }

            Console.Write("Training Point Per Class;");
            Console.Write("Test Point Per Class;");
            Console.Write("build times: Gauss;build times: Bayes;");
            Console.Write("classification times: Gauss;classification times: Bayes;");
            Console.Write("bayes errors;");
            Console.WriteLine("gauss errors;");

            List<Point> training_set, test_set;
            //out_f.WriteLine("Classifining " + plant + " with rate " + RATE + " and upper boundary " + MAX_POINT_MISS);


            var points =
                (
                    from line in text
                    where line != ""
                    select new Point(line)
                ).ToArray();

            for (int training = 6; training <= 40; training++)
            {
                int samples = 100;

                double[] bayes_errors = new double[samples];
                double[] gauss_errors = new double[samples];

                double[]
                    bayes_class = new double[samples],
                    gauss_class = new double[samples],
                    bayes_time = new double[samples],
                    gauss_time = new double[samples];


                for (int sample = 0; sample < samples; sample++)
                {

                    var class0 = points.Where(p => p.Y == 0).ToList();
                    var class1 = points.Where(p => p.Y == 1).ToList();
                    var class2 = points.Where(p => p.Y == 2).ToList();


                    var classifier = new MultivariateNormalDistributionClassifier(labels.Count, points[0].X_Vector.Count);
                    var bayes = new NaiveBayesClassifier(labels.Count, points[0].X_Vector.Count);

                    training_set = new List<Point>();
                    test_set = new List<Point>();

                    //int j = 1;
                    for (int i = 0; i < training; i++)
                    {
                        int idx = seed.Next(0, class0.Count);
                        training_set.Add(class0[idx]);
                        class0.RemoveAt(idx);

                        idx = seed.Next(0, class1.Count);
                        training_set.Add(class1[idx]);
                        class1.RemoveAt(idx);

                        idx = seed.Next(0, class2.Count);
                        training_set.Add(class2[idx]);
                        class2.RemoveAt(idx);
                        //if (j <= training)
                        //{
                        //    training_set.Add(points[i]);
                        //}
                        //else
                        //{
                        //    label_set.Add(points[i]);
                        //}
                        //if (j == 50)
                        //    j = 0;
                    }
                    test_set = class0.Concat(class1).Concat(class2).ToList();
                    var t_set_a = training_set.ToArray();
                    chrono.Restart();
                    classifier.CreateFromPoints(t_set_a);
                    chrono.Stop();

                    gauss_time[sample] = chrono.Elapsed.TotalSeconds;

                    chrono.Restart();
                    bayes.CreateFromPoints(t_set_a);
                    chrono.Stop();
                    bayes_time[sample] = chrono.Elapsed.TotalSeconds;
                    chrono.Restart();
                    foreach (var p in test_set)
                    {
                        int res = classifier.ClassifyThis(p);
                        if (p.Y != res)
                        {
                            gauss_errors[sample]++;
                        }
                    }
                    chrono.Stop();
                    gauss_class[sample] = chrono.Elapsed.TotalSeconds;
                    chrono.Restart();
                    foreach (var p in test_set)
                    {
                        int res = bayes.ClassifyThis(p);
                        if (p.Y != res)
                        {
                            bayes_errors[sample]++;
                        }
                    }
                    chrono.Stop();
                    bayes_class[sample] = chrono.Elapsed.TotalSeconds;

                }
                mean(gauss_time);
                mean(bayes_time);
                mean(gauss_class);
                mean(bayes_class);
                mean(bayes_errors);
                mean(gauss_errors);

                var t_s_l = (points.Length / 3 - training);
                Console.Write(training);
                Console.Write(";" + t_s_l);
                Console.Write(";" + gauss_time[0] * 1000 + ";" + bayes_time[0] * 1000);
                Console.Write(";" + gauss_class[0] * 1000 / t_s_l + ";" + bayes_class[0] * 1000 / t_s_l);
                Console.Write(";" + bayes_errors[0] / t_s_l);
                Console.WriteLine(";" + gauss_errors[0] / t_s_l);
            }
            /*for (int i = 0; i < points.Length; i++)
            //{
            //    //if (i % 3 == 1)
            //    //{
            //    //foreach (var p in label_set)
            //    //{
            //    int res = classifier.ClassifyThis(label_set[i]);
            //    if (label_set[i].Y != res)
            //    {
            //        Console.WriteLine("error in Gauss from input: expected : " + points[i].Y + "; found : " + res);
            //    }
            //}
            //for (int i = 0; i < label_set.Count; i++)
            //{
            //    int res = bayes.ClassifyThis(label_set[i]);
            //    if (label_set[i].Y != res)
            //    {
            //        Console.WriteLine("error in Bayes from input: expected : " + points[i].Y + "; found : " + res);
            //    }
            //}
                //}
            //}*/
            try
            {
                out_f.Dispose();
            }
            catch { }
        }

        private static void mean(double[] aList)
        {
            var sum = aList.Sum();
            sum /= aList.Length;
            for (int i = 0; i < aList.Length; i++)
            {
                aList[i] = sum;
            }
        }
    }
#endregion
}
